/*
 * Author:  Copyright (c) 2013 Toby
 * E-Mail:  smurffit@gmail.com
 * Web:     https://code.google.com/p/chrome-mfi-filter4kiva/
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

// TODO explain required permissions
// TODO Help tab in options (icon explanation)
// TODO color but don't filter feature
// TODO donation rule
// TODO would be nice if config via rightclick -> partner on page could be done

/* ----- FUNC ----- */

/**
 * Configuration object
 * @param {boolean}
 *            block whether filtering is enabled
 * @param {boolean}
 *            highlight whether highlighting is enabled
 * @param regExFilter
 *            {String} the RegExp used to filter MFIs
 * @param regExHighlight
 *            {String} the RegExp used to highlight MFIs
 * @param colorFilteredHide
 *            {String} the color for filtered and hidden items
 * @param colorFilteredShow
 *            {String} the color for filtered but displayed items
 * @param colorHighlighted
 *            {String} the color for highlighted items
 * @param showRatingStars
 *            {boolean} whether the rating stars should be displayed
 * @param autoUpdate
 *            {boolean} whether the partner list and ratings should be automatically fetched
 * @param enableAa
 *            {boolean} whether the AASFSHNR should be fetched
 * @struct
 */
function conf(block, highlight, regExFilter, regExHighlight, colorFilteredHide, colorFilteredShow,
		colorHighlighted, showRatingStars, autoUpdate, enableAa)
{
	this.block = block;
	this.highlight = highlight;
	this.regExFilter = regExFilter;
	this.regExHighlight = regExHighlight;
	this.colorFilteredHide = colorFilteredHide;
	this.colorFilteredShow = colorFilteredShow;
	this.colorHighlighted = colorHighlighted;
	this.showRatingStars = showRatingStars;
	this.autoUpdate = autoUpdate;
	this.enableAa = enableAa;
}

/* ----- END FUNC ----- */

/* ----- VARS ----- */

/**
 * @type number 0 = off, 1 = verbose, 2 = debug
 */
var DEBUG = 0;

/* whether the filtering and/or highlighting are enabled */
var config = new conf(true, true, '', '', 'display: none', 'background-color: #FFDAB9',
		'background-color: lightblue', true, true, true);

/* 0 = initialization required, 1 = ready */
var ready = 0;
var canvas, context, gradient, icon, ratings, res, urlIcon;
var storage = chrome.storage.local;

/* timestamp of the last auto-update */
var mfiUpdateTimeStamp = 0;

/* whether the options tab should be closed automatically (for auto-updates) */
var optionsAutoClose = false;

/* ----- END VARS ----- */

/**
 * Loads an external, extension-registered file into this page
 * 
 * @param {String}
 *            file the name of the file like "db.json"
 * @returns {String} the file content
 */
function readfile(file)
{
	var xhr = new XMLHttpRequest();
	xhr.open('GET', chrome.extension.getURL(file), false);
	xhr.send();
	return xhr.responseText;
}

/**
 * Called whenever the state of the filter is switched between enabled/disabled to save the changes
 * and update the icon
 * 
 * @param {boolean}
 *            state whether the filter is enabled
 */
function toggleFilterState(state)
{
	config.block = state;
	checkState();
	saveConfig();
	chrome.tabs.getSelected(null, function(tab)
	{
		if (!config.block)
		{
			if (!config.highlight)
			{
				setTabProperties(tab.id, '', 'E1', false, '');
			}
			else
			{
				setTabProperties(tab.id, '', '-', false, '');
			}
		}
		else
		{
			setTabProperties(tab.id, '', 0, false, '');
		}
	});
}

/**
 * Sets the variable which holds the current state of the show/hide filtered results
 * 
 * @param {boolean}
 *            showFiltered whether filtered results are currently displayed or not
 */
function toggleHiddenState(showFiltered)
{
	chrome.tabs.getSelected(null, function(tab)
	{
		chrome.tabs.sendMessage(tab.id, {
			action : 'colorHidden',
			value : showFiltered
		});
	});
}

/**
 * Returns whether filtered items are currently shown on this tab or not
 * @param callback
 *            {Object} callback
 */
function getHiddenState(callback)
{
	chrome.tabs.getSelected(null, function(tab)
	{
		chrome.tabs.sendMessage(tab.id, {
			action : 'hiddenShown'
		}, function(response)
		{
			callback(response.result);
		});
	});
}

/**
 * Loads and returns the mfi_list. If this fails the default schema is imported and returned
 * 
 * @param initOnError
 *            {boolean} whether the default schema should be imported on failure
 * @param callback
 *            {Object} callback
 */
function loadMfiList(initOnError, callback)
{
	storage.get('mfi_list', function(items)
	{
		if (items.mfi_list !== undefined)
		{
			res = JSON.parse(items.mfi_list);

			if (DEBUG > 0)
			{
				console.log('DB loaded.');
			}
			storage.get('mfi_list_update_time', function(items)
			{
				if (items.mfi_list_update_time !== undefined)
				{
					mfiUpdateTimeStamp = JSON.parse(items.mfi_list_update_time);
				}
				callback(res);
			});
		}
		else if (initOnError)
		{
			console.log('Unable to load DB, initializing using default values...');
			initDb(function()
			{
				initRegExp(function()
				{
					callback(res);
				});
			});
		}
		else
		{
			callback(undefined);
		}
	});
}

/**
 * Loads and installs the default schema
 * @param callback
 *            {Object} callback
 */
function initDb(callback)
{
	res = JSON.parse(readfile('options/db.json'));
	callback();
}

/**
 * Initializes the two RegExps. Saves the mfi_list and the RegExp to the local storage
 * @param callback
 *            {Object} callback
 */
function initRegExp(callback)
{
	var regExFilter = [];
	var regexHighlight = [];

	for ( var j = 0, lem = res.length; j < lem; j++)
	{
		if (res[j].blocked === true)
		{
			regExFilter.push(res[j].name);
		}
		else if (res[j].highlight === true)
		{
			regexHighlight.push(res[j].name);
		}
	}

	saveMfiList(res, function()
	{
		saveRegEx(regExFilter, regexHighlight, function()
		{
			callback();
		});
	});
}

/**
 * Stores the list of MFIs on the local storage
 * 
 * @param mfiList
 *            {Array} the list of MFIs (not in JSON)
 * @param callback
 *            {Object} callback
 */
function saveMfiList(mfiList, callback)
{
	storage.set({
		'mfi_list' : JSON.stringify(mfiList)
	}, function()
	{
		storage.set({
			'mfi_list_update_time' : JSON.stringify(mfiUpdateTimeStamp)
		}, function()
		{
			callback();
		});
	});
}

/**
 * Stores the RegExp rules for the MFIs to filter and highlight in the locale DB
 * 
 * @param regExBl
 *            {Array} Array with the MFIs to filter
 * @param regExHl
 *            {Array} Array with the MFIs to highlight
 * @param callback
 *            {Object} callback
 */
function saveRegEx(regExBl, regExHl, callback)
{
	config.regExFilter = compileRegEx(regExBl);
	config.regExHighlight = compileRegEx(regExHl);

	saveConfig(callback);
}

/**
 * Creates the RegEx for the filter from the selected MFIs
 * 
 * @param regExSource
 *            {Array} Array of selected (to be filtered) MFIs
 * @returns {String} the RegEx pattern
 */
function compileRegEx(regExSource)
{
	var out = '';
	for ( var i = 0, len = regExSource.length; i < len; i++)
	{
		out = out.concat('|' + escapeRegEx(regExSource[i]));
	}
	return out.substring(1);
}

/**
 * Escapes the MFI names so they can be compiled to RegExps
 * @param s
 *            {String} the MFI name
 * @returns {String} the escaped MFI name, ready to be used for RegExp
 */
function escapeRegEx(s)
{
	return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

/**
 * Initializes the basic background page needed to work properly; like the canvas or rating icons
 * @param callback
 *            {Object} callback
 */
function createBackgroundPage(callback)
{
	if (DEBUG > 0)
	{
		console.log('injecting canvas field.');
	}
	if (ready === 0 || !canvas || !icon)
	{
		canvas = document.getElementById('canvas');
		context = canvas.getContext('2d');
		icon = document.getElementById('icon');
	}

	loadConfig(function()
	{
		config.highlight = (config.regExHighlight !== undefined && (config.regExHighlight.length > 5));
		if (!ratings)
		{
			ratings = getRatingsMap(res);
		}
		ready = 1;
		callback();
	});
}

/**
 * Generates the mapping between the MFIs and their rating
 * @param mfi
 *            {Array} the MFIs
 * @returns {Object} mapping in the fashion of rating[MFI name] -> score
 */
function getRatingsMap(mfi)
{
	var rating = {};
	// XXX using a string as Array index is discouraged
	for ( var i = 0, l = mfi.length; i < l; i++)
	{
		rating[mfi[i].name] = mfi[i].rating;
	}
	return rating;
}

/**
 * Resets the state of the tab
 * @param info
 * @see chrome.webRequest.onCompleted.addListener
 * @param type
 *            {String} type of the content change like 'onCompleted' or 'direct'
 */
function contentChanged(info, type)
{
	if (ready <= 0)
	{
		if (DEBUG > 1)
		{
			console.log('Reinit required.');
		}
		createBackgroundPage(function()
		{
			if (DEBUG > 1)
			{
				console.log('contentChanged');
			}
		});
	}

	if (DEBUG > 1)
	{
		console.log('URL triggered cleanup: ' + info.url);
	}

	chrome.tabs.sendMessage(info.tabId, {
		action : 'reset',
		value : JSON.stringify(config),
		type : type
	});

	if (!config.block)
	{
		setTabProperties(info.tabId, '', '-', false, '');
	}
	else
	{
		setTabProperties(info.tabId, '', 0, false, '0 filtered');
	}
}

/**
 * Generates the icon for the address bar. If the filter is disabled the icon is slightly
 * transparent
 * 
 * @param count
 *            {String} the number to draw on the icon or a character in certain situations (like
 *            errors)
 * @param highlights
 *            {boolean} whether highlighted items are present on this page
 * @returns {Canvas} the icon with the count drawn on it as ImageData
 */
function paintIcon(count, highlights)
{
	canvas.width = canvas.width; // reset current icon
	if (gradient === undefined)
	{
		gradient = context.createLinearGradient(0, 11, 0, 19);
		gradient.addColorStop(0.1, '#CC0000');
		gradient.addColorStop(1, '#770000');
	}

	/* make icon semi-transparent if filter is disabled */
	if (!highlights && (count === '-'))
	{
		context.globalAlpha = 0.5;
	}
	context.drawImage(icon, 0, 0);

	context.globalAlpha = 1; // ensure that number is always opaque
	context.fillStyle = gradient;
	context.fillRect(9, 10, 11, 10);
	context.shadowBlur = 1;
	context.shadowColor = '#222222';
	context.fillStyle = '#FFFFFF';
	context.textAlign = 'center';
	context.textBaseline = 'middle';
	context.font = 'bold 8px arial small-caption';
	context.fillText(count, 14, 16);

	if (highlights)
	{
		context.font = 'bold 16px arial';
		context.fillStyle = 'gold';
		context.fillText('*', 15, 6);
		context.shadowBlur = 2;
		context.shadowColor = 'gold';
	}
	return context.getImageData(0, 0, canvas.width, canvas.height);
}

/**
 * Sets the settings for the icon and the pop-up
 * 
 * @param tabId
 *            {number} the tab id of this call
 * @param url
 *            {String} the filter results
 * @param icon
 *            {String} the result count or error code
 * @param highlights
 *            {boolean} whether highlighted items are present on this page
 * @param title
 *            {String} the title to be set (mouse hover)
 */
function setTabProperties(tabId, url, icon, highlights, title)
{
	if (ready <= 0)
	{
		if (DEBUG > 1)
		{
			console.log('Reinit required.');
		}
		createBackgroundPage(function()
		{
			if (DEBUG > 1)
			{
				console.log('setTabProperties');
			}
		});
	}

	/* if no number is returned, the error code is expanded */
	if (isNaN(icon))
	{
		if (icon === '-')
		{
			title = 'Filter is disabled';
		}
		else if (icon === 'E1')
		{
			icon = '!';
			title = 'Error: no valid filter configured (check options)';
		}
		else if (icon === 'E2')
		{
			icon = '!';
			title = 'Error: unexpected input (try page reload)';
		}
	}

	chrome.pageAction.setPopup({
		tabId : tabId,
		popup : 'popup.html#' + url
	});

	chrome.pageAction.setTitle({
		tabId : tabId,
		title : title
	});

	chrome.pageAction.setIcon({
		tabId : tabId,
		imageData : paintIcon(icon, highlights)
	});

	chrome.pageAction.show(tabId);
}

/**
 * Checks if the RegExp for regExFilter and regExHighlight are valid and sets them to false if not
 * @returns {boolean} the validated state of config.block
 */
function checkState()
{
	if (config.regExFilter === undefined || (config.regExFilter.length < 5))
	{
		config.block = false;
	}
	if (config.regExHighlight === undefined || (config.regExHighlight.length < 5))
	{
		config.highlight = false;
	}
	else
	{
		config.highlight = true;
	}
	return config.block;
}

/**
 * Loads the configuration from local storage or loads the defaults
 * @param callback
 *            {Object} callback
 */
function loadConfig(callback)
{
	storage.get('config', function(items)
	{
		if (items.config !== undefined)
		{
			config = JSON.parse(items.config);
			callback();
		}
		else
		{
			config = new conf(false, false, '', '', 'display: none', 'background-color: peachpuff',
					'background-color: lightblue', true, true, true);
			saveConfig(callback);
		}

		if (DEBUG > 1)
		{
			console.log('Config loaded: ' + config);
		}
	});
}

/**
 * Saves the configuration to the local storage
 * @param callback
 *            {Object} callback
 */
function saveConfig(callback)
{
	storage.set({
		'config' : JSON.stringify(config)
	}, callback);
}

/**
 * Start here...
 */
function start()
{
	loadMfiList(true, function(result)
	{
		createBackgroundPage(function()
		{
			if (config.autoUpdate)
			{
				var d = new Date();
				var lastUpdateDiffMin = ((d.getTime() - mfiUpdateTimeStamp) / 60000).toFixed();
				// 10080 = 7d
				if (lastUpdateDiffMin > 10080)
				{
					console.log('Last Update older than 7 days: ' + lastUpdateDiffMin);
					optionsAutoClose = true;
					chrome.tabs.create({
						url : "options/options.html",
						active : false
					});
				}
			}
		});
	});
}

/* ----- LISTENERS ----- */

chrome.extension.onMessage.addListener(function(result, sender, sendResponse)
{
	if (ready <= 0)
	{
		if (DEBUG > 1)
		{
			console.log('Reinit required.');
		}
		createBackgroundPage(function()
		{
			if (DEBUG > 1)
			{
				console.log('onMessage.addListener');
			}
		});
	}

	if (result.count === 'needRatings')
	{
		chrome.tabs.sendMessage(sender.tab.id, {
			action : 'sendRatings',
			value : JSON.stringify(config),
			rates : JSON.stringify(ratings),
			type : 'feeder'
		});
		return;
	}
	else if (config.highlight && !config.block)
	{
		if (DEBUG > 0)
		{
			console.log('highlights: ' + result.highlights);
		}
		setTabProperties(sender.tab.id, '', '-', result.highlights,
				(result.highlights === true ? 'found prefered Partner' : ''));
	}
	else
	{
		if (DEBUG > 0)
		{
			console.log('filtered: ' + result.count);
		}
		setTabProperties(sender.tab.id, result.filtered, result.count, result.highlights,
				result.count + ' filtered' +
						(result.highlights === true ? ', found prefered Partner' : ''));
	}
});

chrome.webRequest.onCompleted.addListener(function(details)
{
	if (details.statusCode === 200)
	{
		if (DEBUG > 1)
		{
			console.log(details);
		}

		if (details.url.indexOf('queryBusinessesListing') > 0)
		{
			contentChanged(details, 'direct');
		}
		else if (details.url.indexOf('#') === -1 && !details.url.match(/\/lend\/\d{4,}$/))
		{
			// XXX first Lend page still does not work on back navigation
			contentChanged(details, 'onCompleted');
		}
	}
}, {
	urls : [ '*://*.kiva.org/lend*', '*://*.kiva.org/ajax/auto*' ],
	types : [ 'main_frame', 'xmlhttprequest' ]
});

/* Ensure expected state on install/start */
chrome.runtime.onStartup.addListener(function()
{
	start();
});

chrome.runtime.onInstalled.addListener(function()
{
	start();
});

/* ----- END LISTENERS ----- */
